﻿namespace Patterns4Net.Core.CecilExtensions
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.Contracts;
    using System.Linq;
    using System.Reflection;
    using System.Text.RegularExpressions;
    using Mono.Cecil;
    using Mono.Cecil.Rocks;

    public static class ReflectionToCecil
    {
        public static ModuleDefinition GetModuleDefinition(this Assembly assembly)
        {
            Contract.Requires(assembly != null);
            Contract.Ensures(Contract.Result<object>() != null);

            ModuleDefinition module = null;
            try
            {
                module = ModuleDefinition.ReadModule(assembly.Location);
            }
            #region ExceptionHandling
            catch (BadImageFormatException exception)
            {
                throw new GetModuleException(
                    assembly,
                    "ReadModule has thrown BadImageFormatException. File with assembly data seems to be corrupted.",
                    exception);
            }
            catch (ArgumentException exception)
            {
                throw new GetModuleException(
                    assembly,
                    "ReadModule has thrown ArgumentException. Some contract on arguments inside Mono.Cecil has been probably broken.",
                    exception);
            }
            catch (Exception exception)
            {
                var message =
                    string.Format(
                        "ReadModule has thrown {0} which was not expected and so catched in general Exception catch block. No additional info cannot be provided.",
                        exception.GetType().Name);
                throw new GetModuleException(assembly, message, exception);
            }
            #endregion

            if (module == null)
            {
                throw new GetModuleException(assembly, "ReadModule has returned a null reference and haven't thrown any exception.");
            }

            return module;
        }

        public static TypeDefinition GetTypeDefinition(this Type type)
        {
            Contract.Requires(type != null);
            Contract.Requires(
                type.IsGenericType == false || type.IsGenericTypeDefinition,
                "Given type should not be instance of generic type e. g. typeof(List<int>) is OK, " + 
                "but typeof(List<>) is not. Use GetTypeReference() instead.");
            Contract.Ensures(Contract.Result<TypeDefinition>() != null);
            Contract.Assert(type.Assembly != null);
            Contract.Assert(type.FullName != null);

            var name = GetCecilTypeName(type.FullName); 
            var module = type.Assembly.GetModuleDefinition();

            var result = module.GetType(name);            
            if (result == null)
            {
                throw new GetTypeDefinitionException(type, name, "GetType has returned null reference.");
            }

            return result;
        }

        public static TypeReference GetTypeReference(this Type type)
        {
            Contract.Requires(type != null);
            Contract.Ensures(Contract.Result<TypeReference>() != null);

            if (type.IsGenericType && type.IsGenericTypeDefinition == false)
            {
                // the type is generic type with concrete generic args like List<int>
                return GetGenericTypeDefinition(type);
            }
            else
            {
                return type.GetTypeDefinition();
            }
        }

        private static TypeReference GetGenericTypeDefinition(Type type)
        {
            var result = type.GetGenericTypeDefinition().GetTypeDefinition();
            var args = type.GetGenericArguments().Select(t => t.GetTypeReference());
            return result.MakeGenericInstanceType(args.ToArray());
        }

        internal static string GetCecilTypeName(string reflectionTypeName)
        {
            Contract.Requires(reflectionTypeName != null);
            Contract.Ensures(Contract.Result<string>() != null);
            // Cecil uses slash for nested types, reflection uses plus
            return reflectionTypeName.Replace("+", "/");
        }

        public class GetTypeDefinitionException : ApplicationException
        {            
            private const string MessageTemplate =
                "Error in extension method GetTypeDefinition (declared in class ReflectionToCecil)." +
                "The method was called with parameter equal to System.Type '{0}' and in it's " +
                "body called Mono.Cecil.ModuleDefinition.GetType with parameter equal to '{1}'. {2}";

            public GetTypeDefinitionException(Type type, string getTypeParam, string info, Exception inner = null)
                : base(string.Format(MessageTemplate, type.FullName, getTypeParam, info), inner)
            {
                Contract.Requires(type != null);
                this.Type = type;
            }

            public Type Type { get; private set; }
        }

        public class GetModuleException : ApplicationException
        {
            private const string MessageTemplate =
                "Error in extension method GetModuleDefinition (declared in class ReflectionToCecil). " +
                "The method was called with parameter equal to assembly '{0}' and in its body the method " +
                "called static method Mono.Cecil.ModuleDefinition.ReadModule with parameter equal to " +
                "assembly location '{1}'. {2}";

            public GetModuleException(Assembly assembly, string info, Exception inner = null)
                : base(string.Format(MessageTemplate, assembly.FullName, assembly.Location, info), inner)
            {
                Contract.Requires(assembly != null);
                this.Assembly = assembly;
            }

            public Assembly Assembly { get; private set; }
        }
    }
}
